﻿using System;
using System.IO;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;

namespace AMatysiak.Methods
{
    public static class FormatNamedExtension
    {
        private static State state;
        private static StringBuilder result;
        private static StringBuilder expression;

        private abstract class State
        {
            protected int @char;

            public void Action(int @char)
            {
                this.@char = @char;
                switch (@char)
                {
                    case '{': OnOpenBracket(); break;
                    case '}': OnCloseBracket(); break;
                    case -1: End(); break;
                    default: DefaultAction(); break;
                }
            }

            public virtual bool IsNotEnd()
            {
                return true;
            }

            protected abstract void DefaultAction();

            protected virtual void OnOpenBracket() { DefaultAction(); }
            protected virtual void OnCloseBracket() { DefaultAction(); }
            protected virtual void End() { DefaultAction(); }
        }

        private class OutsideExpressionState : State
        {
            protected override void DefaultAction()
            {
                result.Append((char)@char);
            }

            protected override void OnOpenBracket()
            {
                state = new OpenBracketState();
            }

            protected override void OnCloseBracket()
            {
                state = new CloseBracketState();
            }

            protected override void End()
            {
                state = new EndState();
            }
        }

        private class OpenBracketState : State
        {
            protected override void DefaultAction()
            {
                expression.Append((char)@char);
                state = new InsideExpressionState();
            }

            protected override void OnOpenBracket()
            {
                result.Append('{');
                state = new OutsideExpressionState();
            }

            protected override void End()
            {
                throw new FormatException();
            }
        }

        private class CloseBracketState : State
        {
            protected override void DefaultAction()
            {
                throw new FormatException();
            }

            protected override void OnCloseBracket()
            {
                result.Append('}');
                state = new OutsideExpressionState();
            }
        }

        private class InsideExpressionState : State
        {
            protected override void DefaultAction()
            {
                expression.Append((char)@char);
            }

            protected override void OnCloseBracket()
            {
                result.Append(outExpression());
                expression = new StringBuilder();
                state = new OutsideExpressionState();
            }

            private string outExp;
            private string originalExp;
            private string formatExp;
            private object valueExp;

            private string outExpression()
            {
                initializeOutExp();

                splitByColon();

                try
                {
                    return bindData();
                }
                catch (Exception)
                {
                    return handleException();
                }
            }

            private void initializeOutExp()
            {
                outExp = expression.ToString();
                originalExp = outExp;
                formatExp = "";
            }

            private void splitByColon()
            {
                int colonIndex = outExp.IndexOf(':');
                if (colonIndex > 0)
                {
                    formatExp = outExp.Substring(colonIndex + 1);
                    outExp = outExp.Substring(0, colonIndex);
                }
            }

            private string bindData()
            {
                string value = bindDataToText();
                convertToValueExp(value);
                value = formatValue();

                return value;
            }

            MethodInfo method;
            object ndx;

            private string bindDataToText()
            {
                Type sourceType = source.GetType();

                PropertyInfo property = sourceType.GetProperty(outExp);

                if (property != null)
                    return property.GetValue(source, null).ToString();

                List<MethodInfo> methods = sourceType.GetMethods().Where(x => x.Name.Contains("get_Item")).ToList();

                if (methods.Count() > 0)
                {
                    getBestFitIndexer(methods);
                    return method.Invoke(source, new object[] { ndx }).ToString();
                }

                throw new Exception("No valid property or indexer.");
            }

            private void getBestFitIndexer(List<MethodInfo> methods)
            {
                Dictionary<Type, MethodInfo> typedMethods = getTypedIndexers(methods);

                Type outExpType = getTypeOfValue(outExp);

                if (typedMethods.ContainsKey(outExpType))
                    method = typedMethods[outExpType];
                else
                    method = typedMethods[typeof(string)];

                if (method == null)
                    method = methods[0];

                Type type = getTypeOfIndexer(method);
                ndx = Convert.ChangeType(outExp, type);
            }

            private Dictionary<Type, MethodInfo> getTypedIndexers(List<MethodInfo> methods)
            {
                Dictionary<Type, MethodInfo> list = new Dictionary<Type, MethodInfo>();

                foreach (MethodInfo method in methods)
                    list.Add(getTypeOfIndexer(method), method);

                return list;
            }

            private Type getTypeOfIndexer(MethodInfo mi)
            {
                return mi.GetParameters().First().ParameterType;
            }

            private Type getTypeOfValue(string value)
            {
                int testInt;
                double testDouble;

                if (int.TryParse(value, out testInt))
                    return typeof(int);
                else if (double.TryParse(value, out testDouble))
                    return typeof(double);
                else
                    return typeof(string);
            }

            private void convertToValueExp(string value)
            {
                Type type = getTypeOfValue(value);

                if (type == typeof(int))
                    valueExp = int.Parse(value);
                else if (type == typeof(double))
                    valueExp = double.Parse(value);
                else
                    valueExp = value;
            }

            private string formatValue()
            {
                if (formatExp.IsEmpty())
                    return valueExp.ToString();

                string formatted = "{0:" + formatExp + "}";
                return formatted.FormatWith(valueExp);
            }

            private string handleException()
            {
                if (!throwException)
                    throw new FormatException();
                else
                    return originalExp;
            }

            protected override void End()
            {
                throw new FormatException();
            }
        }

        private class EndState : State
        {
            protected override void DefaultAction()
            {

            }

            public override bool IsNotEnd()
            {
                return false;
            }
        }

        private static string format;
        private static object source;
        private static bool throwException;

        public static string FormatNamed(this string format, object source)
        {
            return FormatNamed(format, source, false);
        }

        public static string FormatNamed(this string format, object source, bool throwException)
        {
            initialize(format, source, throwException);

            analyzePattern();

            return formatedResult();
        }

        private static void initialize(string f, object s, bool te)
        {
            if (formatIsInvalid(f))
                throw new ArgumentNullException("Empty pattern");

            format = f;
            source = s;
            throwException = te;

            result = new StringBuilder(format.Length * 2);
        }

        private static void analyzePattern()
        {
            using (var reader = new StringReader(format))
            {
                readFormatPattern(reader);
            }
        }

        private static void readFormatPattern(StringReader reader)
        {
            initializeStateReader();

            analyzePattern(reader);
        }

        private static int @char;

        private static void initializeStateReader()
        {
            @char = -1;
            expression = new StringBuilder();
            state = new OutsideExpressionState();
        }

        private static void analyzePattern(StringReader reader)
        {
            do
            {
                readChar(reader);
                actionOfState();
            } while (state.IsNotEnd());
        }

        private static void readChar(StringReader reader)
        {
            @char = reader.Read();
        }

        private static void actionOfState()
        {
            state.Action(@char);
        }

        private static bool formatIsInvalid(string format)
        {
            return format == null;
        }

        private static string formatedResult()
        {
            return result.ToString();
        }
    }
}
